//===--- ImportEnumInfo.cpp - Information about importable Clang enums ----===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// This file provides EnumInfo, which describes a Clang enum ready to be
// imported
//
//===----------------------------------------------------------------------===//

#include "polarphp/clangimporter/internal/ClangAdapter.h"
#include "polarphp/clangimporter/internal/ImportEnumInfo.h"
#include "polarphp/clangimporter/internal/ImporterImpl.h"
#include "polarphp/basic/StringExtras.h"
#include "polarphp/llparser/Lexer.h"

#include "clang/AST/Attr.h"
#include "clang/AST/Decl.h"
#include "clang/Lex/MacroInfo.h"
#include "clang/Lex/Preprocessor.h"

#include "llvm/ADT/Statistic.h"
#define DEBUG_TYPE "Enum Info"
STATISTIC(EnumInfoNumCacheHits, "# of times the enum info cache was hit");
STATISTIC(EnumInfoNumCacheMisses, "# of times the enum info cache was missed");

using namespace polar;
using namespace importer;
using namespace llparser;

/// Find the last extensibility attribute on \p decl as arranged by source
/// location...unless there's an API note, in which case that one wins.
///
/// This is not what Clang will do, but it's more useful for us since CF_ENUM
/// already has enum_extensibility(open) in it.
static clang::EnumExtensibilityAttr *
getBestExtensibilityAttr(clang::Preprocessor &pp, const clang::EnumDecl *decl) {
   clang::EnumExtensibilityAttr *bestSoFar = nullptr;
   const clang::SourceManager &sourceMgr = pp.getSourceManager();
   for (auto *next : decl->specific_attrs<clang::EnumExtensibilityAttr>()) {
      if (next->getLocation().isInvalid()) {
         // This is from API notes -- use it!
         return next;
      }

      if (!bestSoFar ||
          sourceMgr.isBeforeInTranslationUnit(bestSoFar->getLocation(),
                                              next->getLocation())) {
         bestSoFar = next;
      }
   }
   return bestSoFar;
}

/// Classify the given Clang enumeration to describe how to import it.
void EnumInfo::classifyEnum(const clang::EnumDecl *decl,
                            clang::Preprocessor &pp) {
   assert(decl);
   clang::PrettyStackTraceDecl trace(decl, clang::SourceLocation(),
                                     pp.getSourceManager(), "classifying");
   assert(decl->isThisDeclarationADefinition());

   // Anonymous enumerations simply get mapped to constants of the
   // underlying type of the enum, because there is no way to conjure up a
   // name for the Swift type.
   if (!decl->hasNameForLinkage()) {
      kind = EnumKind::Constants;
      return;
   }

   // First, check for attributes that denote the classification.
   // @TODO
//   if (auto domainAttr = decl->getAttr<clang::NSErrorDomainAttr>()) {
//      kind = EnumKind::NonFrozenEnum;
//      nsErrorDomain = domainAttr->getErrorDomain()->getName();
//   }
   if (decl->hasAttr<clang::FlagEnumAttr>()) {
      kind = EnumKind::Options;
      return;
   }
   if (auto *attr = getBestExtensibilityAttr(pp, decl)) {
      if (attr->getExtensibility() == clang::EnumExtensibilityAttr::Closed)
         kind = EnumKind::FrozenEnum;
      else
         kind = EnumKind::NonFrozenEnum;
      return;
   }
   if (!nsErrorDomain.empty())
      return;
   // @todo
   // If API notes have /removed/ a FlagEnum or EnumExtensibility attribute,
   // then we don't need to check the macros.
//   for (auto *attr : decl->specific_attrs<clang::SwiftVersionedAttr>()) {
//      if (!attr->getIsReplacedByActive())
//         continue;
//      if (isa<clang::FlagEnumAttr>(attr->getAttrToAdd()) ||
//          isa<clang::EnumExtensibilityAttr>(attr->getAttrToAdd())) {
//         kind = EnumKind::Unknown;
//         return;
//      }
//   }

   // Was the enum declared using *_ENUM or *_OPTIONS?
   // FIXME: Stop using these once flag_enum and enum_extensibility
   // have been adopted everywhere, or at least relegate them to Swift 4 mode
   // only.
   auto loc = decl->getBeginLoc();
   if (loc.isMacroID()) {
      StringRef MacroName = pp.getImmediateMacroName(loc);
      if (MacroName == "CF_ENUM" || MacroName == "__CF_NAMED_ENUM" ||
          MacroName == "OBJC_ENUM" || MacroName == "TYPEPHP_ENUM" ||
          MacroName == "TYPEPHP_ENUM_NAMED") {
         kind = EnumKind::NonFrozenEnum;
         return;
      }
      if (MacroName == "CF_OPTIONS" || MacroName == "OBJC_OPTIONS" ||
          MacroName == "TYPEPHP_OPTIONS") {
         kind = EnumKind::Options;
         return;
      }
   }

   // Hardcode a particular annoying case in the OS X headers.
   if (decl->getName() == "DYLD_BOOL") {
      kind = EnumKind::FrozenEnum;
      return;
   }

   // Fall back to the 'Unknown' path.
   kind = EnumKind::Unknown;
}

/// Returns the common prefix of two strings at camel-case word granularity.
///
/// For example, given "NSFooBar" and "NSFooBas", returns "NSFoo"
/// (not "NSFooBa"). The returned StringRef is a slice of the "a" argument.
///
/// If either string has a non-identifier character immediately after the
/// prefix, \p followedByNonIdentifier will be set to \c true. If both strings
/// have identifier characters after the prefix, \p followedByNonIdentifier will
/// be set to \c false. Otherwise, \p followedByNonIdentifier will not be
/// changed from its initial value.
///
/// This is used to derive the common prefix of enum constants so we can elide
/// it from the Swift interface.
StringRef importer::getCommonWordPrefix(StringRef a, StringRef b,
                                        bool &followedByNonIdentifier) {
   auto aWords = camel_case::getWords(a), bWords = camel_case::getWords(b);
   auto aI = aWords.begin(), aE = aWords.end(), bI = bWords.begin(),
      bE = bWords.end();

   unsigned prevLength = 0;
   unsigned prefixLength = 0;
   for (; aI != aE && bI != bE; ++aI, ++bI) {
      if (*aI != *bI) {
         followedByNonIdentifier = false;
         break;
      }

      prevLength = prefixLength;
      prefixLength = aI.getPosition() + aI->size();
   }

   // Avoid creating a prefix where the rest of the string starts with a number.
   if ((aI != aE && !Lexer::isIdentifier(*aI)) ||
       (bI != bE && !Lexer::isIdentifier(*bI))) {
      followedByNonIdentifier = true;
      prefixLength = prevLength;
   }

   return a.slice(0, prefixLength);
}

/// Returns the common word-prefix of two strings, allowing the second string
/// to be a common English plural form of the first.
///
/// For example, given "NSProperty" and "NSProperties", the full "NSProperty"
/// is returned. Given "NSMagicArmor" and "NSMagicArmory", only
/// "NSMagic" is returned.
///
/// The "-s", "-es", and "-ies" patterns cover every plural NS_OPTIONS name
/// in Cocoa and Cocoa Touch.
///
/// \see getCommonWordPrefix
StringRef importer::getCommonPluralPrefix(StringRef singular,
                                          StringRef plural) {
   assert(!plural.empty());

   if (singular.empty())
      return singular;

   bool ignored;
   StringRef commonPrefix = getCommonWordPrefix(singular, plural, ignored);
   if (commonPrefix.size() == singular.size() || plural.back() != 's')
      return commonPrefix;

   StringRef leftover = singular.substr(commonPrefix.size());
   StringRef firstLeftoverWord = camel_case::getFirstWord(leftover);
   StringRef commonPrefixPlusWord =
      singular.substr(0, commonPrefix.size() + firstLeftoverWord.size());

   // Is the plural string just "[singular]s"?
   plural = plural.drop_back();
   if (plural.endswith(firstLeftoverWord))
      return commonPrefixPlusWord;

   if (plural.empty() || plural.back() != 'e')
      return commonPrefix;

   // Is the plural string "[singular]es"?
   plural = plural.drop_back();
   if (plural.endswith(firstLeftoverWord))
      return commonPrefixPlusWord;

   if (plural.empty() || !(plural.back() == 'i' && singular.back() == 'y'))
      return commonPrefix;

   // Is the plural string "[prefix]ies" and the singular "[prefix]y"?
   plural = plural.drop_back();
   firstLeftoverWord = firstLeftoverWord.drop_back();
   if (plural.endswith(firstLeftoverWord))
      return commonPrefixPlusWord;

   return commonPrefix;
}

/// Determine the prefix to be stripped from the names of the enum constants
/// within the given enum.
void EnumInfo::determineConstantNamePrefix(const clang::EnumDecl *decl) {
   switch (getKind()) {
      case EnumKind::NonFrozenEnum:
      case EnumKind::FrozenEnum:
      case EnumKind::Options:
         // Enums are mapped to Swift enums, Options to Swift option sets, both
         // of which attempt prefix-stripping.
         break;

      case EnumKind::Constants:
      case EnumKind::Unknown:
         // Nothing to do.
         return;
   }

   // If there are no enumers, there is no prefix to compute.
   auto ec = decl->enumerator_begin(), ecEnd = decl->enumerator_end();
   if (ec == ecEnd)
      return;

   // Determine whether the given enumerator is non-deprecated and has no
   // specifically-provided name.
   auto isNonDeprecatedWithoutCustomName = [](
      const clang::EnumConstantDecl *elem) -> bool {
//       @todo
//      if (elem->hasAttr<clang::SwiftNameAttr>())
//         return false;

      llvm::VersionTuple maxVersion{~0U, ~0U, ~0U};
      switch (elem->getAvailability(nullptr, maxVersion)) {
         case clang::AR_Available:
         case clang::AR_NotYetIntroduced:
            for (auto attr : elem->attrs()) {
               if (auto annotate = dyn_cast<clang::AnnotateAttr>(attr)) {
                  if (annotate->getAnnotation() == "swift1_unavailable")
                     return false;
               }
               if (auto avail = dyn_cast<clang::AvailabilityAttr>(attr)) {
                  if (avail->getPlatform()->getName() == "swift")
                     return false;
               }
            }
            return true;

         case clang::AR_Deprecated:
         case clang::AR_Unavailable:
            return false;
      }

      llvm_unreachable("Invalid AvailabilityAttr.");
   };

   // Move to the first non-deprecated enumerator, or non-swift_name'd
   // enumerator, if present.
   auto firstNonDeprecated =
      std::find_if(ec, ecEnd, isNonDeprecatedWithoutCustomName);
   bool hasNonDeprecated = (firstNonDeprecated != ecEnd);
   if (hasNonDeprecated) {
      ec = firstNonDeprecated;
   } else {
      // Advance to the first case without a custom name, deprecated or not.
      // @todo
//      while (ec != ecEnd && (*ec)->hasAttr<clang::SwiftNameAttr>())
//         ++ec;
      if (ec == ecEnd) {
         return;
      }
   }

   // Compute the common prefix.
   StringRef commonPrefix = (*ec)->getName();
   bool followedByNonIdentifier = false;
   for (++ec; ec != ecEnd; ++ec) {
      // Skip deprecated or swift_name'd enumerators.
      const clang::EnumConstantDecl *elem = *ec;
      if (hasNonDeprecated) {
         if (!isNonDeprecatedWithoutCustomName(elem))
            continue;
      }
      // @todo
      /*else {
         if (elem->hasAttr<clang::SwiftNameAttr>())
            continue;
      }*/

      commonPrefix = getCommonWordPrefix(commonPrefix, elem->getName(),
                                         followedByNonIdentifier);
      if (commonPrefix.empty())
         break;
   }

   if (!commonPrefix.empty()) {
      StringRef checkPrefix = commonPrefix;

      // Account for the 'kConstant' naming convention on enumerators.
      if (checkPrefix[0] == 'k') {
         bool canDropK;
         if (checkPrefix.size() >= 2)
            canDropK = clang::isUppercase(checkPrefix[1]);
         else
            canDropK = !followedByNonIdentifier;

         if (canDropK)
            checkPrefix = checkPrefix.drop_front();
      }

      // Don't use importFullName() here, we want to ignore the swift_name
      // and swift_private attributes.
      StringRef enumNameStr = decl->getName();
      if (enumNameStr.empty())
         enumNameStr = decl->getTypedefNameForAnonDecl()->getName();
      assert(!enumNameStr.empty() && "should have been classified as Constants");

      StringRef commonWithEnum = getCommonPluralPrefix(checkPrefix, enumNameStr);
      size_t delta = commonPrefix.size() - checkPrefix.size();

      // Account for the 'EnumName_Constant' convention on enumerators.
      if (commonWithEnum.size() < checkPrefix.size() &&
          checkPrefix[commonWithEnum.size()] == '_' && !followedByNonIdentifier) {
         delta += 1;
      }

      commonPrefix = commonPrefix.slice(0, commonWithEnum.size() + delta);
   }

   constantNamePrefix = commonPrefix;
}

EnumInfo EnumInfoCache::getEnumInfo(const clang::EnumDecl *decl) {
   auto iter = enumInfos.find(decl);
   if (iter != enumInfos.end()) {
      ++EnumInfoNumCacheHits;
      return iter->second;
   }
   ++EnumInfoNumCacheMisses;
   EnumInfo enumInfo(decl, clangPP);
   enumInfos[decl] = enumInfo;
   return enumInfo;
}
